/*
 * Copyright 2011 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package sweezy.swf.encoder
{
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import sweezy.swf.action.IAction;
	import sweezy.swf.action.code.ActionCall;
	import sweezy.swf.action.code.ActionConstantPool;
	import sweezy.swf.action.code.ActionDefineFunction;
	import sweezy.swf.action.code.ActionDefineFunction2;
	import sweezy.swf.action.code.ActionGetURL;
	import sweezy.swf.action.code.ActionGetURL2;
	import sweezy.swf.action.code.ActionGoToLabel;
	import sweezy.swf.action.code.ActionGotoFrame;
	import sweezy.swf.action.code.ActionGotoFrame2;
	import sweezy.swf.action.code.ActionIf;
	import sweezy.swf.action.code.ActionJump;
	import sweezy.swf.action.code.ActionPush;
	import sweezy.swf.action.code.ActionPushValue;
	import sweezy.swf.action.code.ActionSetTarget;
	import sweezy.swf.action.code.ActionStoreRegister;
	import sweezy.swf.action.code.ActionTry;
	import sweezy.swf.action.code.ActionUnknown;
	import sweezy.swf.action.code.ActionWaitForFrame;
	import sweezy.swf.action.code.ActionWaitForFrame2;
	import sweezy.swf.action.code.ActionWith;
	import sweezy.swf.constant.ActionPushType;
	import sweezy.swf.constant.SWFVersion;
	import sweezy.swf.io.SWFOutput;
	import sweezy.swf.structure.RegisterParam;
	import sweezy.swf.visitor.SWFVisitor;

	internal class ActionEncoder extends SWFVisitor
	{

		private var _bytesPool:Array;

		private var _offset:uint;

		private var _out:SWFOutput;

		private var _version:uint;

		public function ActionEncoder(version:uint, bytes:ByteArray)
		{
			_version = version;
			_out = new SWFOutput(bytes);
			_bytesPool = [];
		}

		public function visit(element:IAction):void
		{
			var code:uint = element.code;
			_out.writeUI8(code);

			if (code < 0x80)
			{
				return;
			}

			pushBytes();
			element.accept(this);
			var bytes:ByteArray = popBytes();
			_out.writeUI16(bytes.length - _offset);
			_out.writeBytes(bytes);

			_offset = 0;
		}

		override public function visitActionCall(element:ActionCall):void
		{
		}

		override public function visitActionConstantPool(element:ActionConstantPool):void
		{
			_out.writeUI16(element.constantPool.length);
			for each (var str:String in element.constantPool)
			{
				writeString(str);
			}
		}

		override public function visitActionDefineFunction(element:ActionDefineFunction):void
		{
			writeString(element.name);
			_out.writeUI16(element.params.length);
			for each (var param:String in element.params)
			{
				writeString(param);
			}

			pushBytes();
			for each (var action:IAction in element.actions)
			{
				visit(action);
			}

			var bytes:ByteArray = popBytes();
			_out.writeUI16(bytes.length);
			_out.writeBytes(bytes);

			_offset = bytes.length;
		}

		override public function visitActionDefineFunction2(element:ActionDefineFunction2):void
		{
			writeString(element.functionName);
			_out.writeUI16(element.parameters.length);
			_out.writeUI8(element.registerCount);
			_out.writeFLAG(element.preloadParent);
			_out.writeFLAG(element.preloadRoot);
			_out.writeFLAG(element.suppressSuper);
			_out.writeFLAG(element.preloadSuper);
			_out.writeFLAG(element.suppressArguments);
			_out.writeFLAG(element.preloadArguments);
			_out.writeFLAG(element.suppressThis);
			_out.writeFLAG(element.preloadThis);
			_out.writeUB(7, 0);
			_out.writeFLAG(element.preloadGlobal);

			for each (var registerParam:RegisterParam in element.parameters)
			{
				_out.writeUI8(registerParam.regster);
				writeString(registerParam.paramName);
			}

			pushBytes();
			for each (var action:IAction in element.actions)
			{
				visit(action);
			}

			var bytes:ByteArray = popBytes();
			_out.writeUI16(bytes.length);
			_out.writeBytes(bytes);

			_offset = bytes.length;
		}

		override public function visitActionGetURL(element:ActionGetURL):void
		{
			writeString(element.urlString);
			writeString(element.targetString);
		}

		override public function visitActionGetURL2(element:ActionGetURL2):void
		{
			_out.writeUB(2, element.sendVarsMethod);
			_out.writeUB(4, 0);
			_out.writeFLAG(element.loadTarget);
			_out.writeFLAG(element.loadVariables);
		}

		override public function visitActionGoToLabel(element:ActionGoToLabel):void
		{
			writeString(element.label);
		}

		override public function visitActionGotoFrame(element:ActionGotoFrame):void
		{
			_out.writeUI16(element.frame);
		}

		override public function visitActionGotoFrame2(element:ActionGotoFrame2):void
		{
			_out.writeUB(6, 0);
			_out.writeFLAG(element.hasSceneBias);
			_out.writeFLAG(element.play);
			if (element.hasSceneBias)
			{
				_out.writeUI16(element.sceneBias);
			}
		}

		override public function visitActionIf(element:ActionIf):void
		{
			_out.writeSI16(element.branchOffset);
		}

		override public function visitActionJump(element:ActionJump):void
		{
			_out.writeSI16(element.branchOffset);
		}

		override public function visitActionPush(element:ActionPush):void
		{
			for each (var value:ActionPushValue in element.values)
			{
				_out.writeUI8(value.type);

				switch (value.type)
				{
					case ActionPushType.STRING:
						writeString(value.string);
						break;
					case ActionPushType.FLOAT:
						_out.writeFLOAT32(value.float);
						break;
					case ActionPushType.REGISTER_NUMBER:
						_out.writeUI8(value.registerNumber);
						break;
					case ActionPushType.BOOLEAN:
						_out.writeUI8(value.boolean ? 1 : 0);
						break;
					case ActionPushType.DOUBLE:
						_out.writeDOUBLE(value.double);
						break;
					case ActionPushType.INTEGER:
						_out.writeUI32(value.integer);
						break;
					case ActionPushType.CONSTANT_8:
						_out.writeUI8(value.constant8);
						break;
					case ActionPushType.CONSTANT_16:
						_out.writeUI16(value.constant16);
						break;
				}
			}
		}

		override public function visitActionSetTarget(element:ActionSetTarget):void
		{
			writeString(element.targetName);
		}

		override public function visitActionStoreRegister(element:ActionStoreRegister):void
		{
			_out.writeUI8(element.registerNumber);
		}

		override public function visitActionTry(element:ActionTry):void
		{
			var action:IAction;

			_out.writeUB(5, 0);
			_out.writeFLAG(element.catchInRegister);
			_out.writeFLAG(element.finallyBlockFlag);
			_out.writeFLAG(element.catchBlockFlag);

			pushBytes();
			for each (action in element.tryBody)
			{
				visit(action);
			}

			pushBytes();
			for each (action in element.catchBody)
			{
				visit(action);
			}

			pushBytes();
			for each (action in element.finallyBody)
			{
				visit(action);
			}

			var finallyBytes:ByteArray = popBytes();
			var catchBytes:ByteArray = popBytes();
			var tryBytes:ByteArray = popBytes();

			_out.writeUI16(tryBytes.length);
			_out.writeUI16(catchBytes.length);
			_out.writeUI16(finallyBytes.length);

			if (element.catchInRegister)
			{
				_out.writeUI8(element.catchRegister);
			}
			else
			{
				writeString(element.catchName);
			}

			_out.writeBytes(tryBytes);
			_out.writeBytes(catchBytes);
			_out.writeBytes(finallyBytes);

			_offset = tryBytes.length + catchBytes.length + finallyBytes.length;
		}

		override public function visitActionUnknown(element:ActionUnknown):void
		{
			_out.writeBytes(element.operands);
		}

		override public function visitActionWaitForFrame(element:ActionWaitForFrame):void
		{
			_out.writeUI16(element.frame);
			_out.writeUI8(element.skipCount);
		}

		override public function visitActionWaitForFrame2(element:ActionWaitForFrame2):void
		{
			_out.writeUI8(element.skipCount);
		}

		override public function visitActionWith(element:ActionWith):void
		{
			pushBytes();
			for each (var action:IAction in element.actions)
			{
				visit(action);
			}

			var bytes:ByteArray = popBytes();
			_out.writeUI16(bytes.length);
			_out.writeBytes(bytes);

			_offset = bytes.length;
		}

		private function popBytes():ByteArray
		{
			var bytes:ByteArray = _out.popBytes();
			_bytesPool.push(bytes);
			return bytes;
		}

		private function pushBytes():void
		{
			var bytes:ByteArray;

			if (_bytesPool.length === 0)
			{
				bytes = new ByteArray();
				bytes.endian = Endian.LITTLE_ENDIAN;
			}
			else
			{
				bytes = _bytesPool.pop();
				bytes.clear();
			}

			_out.pushBytes(bytes);
		}

		private function writeString(value:String, nullTerminate:Boolean = true):void
		{
			if (_version <= SWFVersion.VERSION_5)
			{
				_out.writeString(value, "shift-jis", nullTerminate);
				return;
			}

			_out.writeString(value, null, nullTerminate);
		}
	}
}
